"""
Tetris
俄罗斯方块

Tetris clone, with some ideas from silvasur's code:
https://gist.github.com/silvasur/565419/d9de6a84e7da000797ac681976442073045c74a4

If Python and Arcade are installed, this example can be run from the command line with:
python -m arcade.examples.tetris
"""
import arcade
from arcade.clock import GLOBAL_CLOCK
import random
import PIL

# Set how many rows and columns we will have
# 共计行的数量是24，列的数量是10
ROW_COUNT = 24
COLUMN_COUNT = 10

# This sets the WIDTH and HEIGHT of each grid location
# 方块的宽高
WIDTH = 30
HEIGHT = 30

# This sets the margin between each cell
# and on the edges of the screen.
# 边界5px
MARGIN = 5

# Do the math to figure out our screen dimensions
WINDOW_WIDTH = (WIDTH + MARGIN) * COLUMN_COUNT + MARGIN
WINDOW_HEIGHT = (HEIGHT + MARGIN) * ROW_COUNT + MARGIN
WINDOW_TITLE = "Tetris"

# 8种颜色
colors = [
    (0,   0,   0, 255),
    (255, 0,   0, 255),
    (0,   150, 0, 255),
    (0,   0,   255, 255),
    (255, 120, 0, 255),
    (255, 255, 0, 255),
    (180, 0,   255, 255),
    (0,   220, 220, 255)
]

# Define the shapes of the single parts
"""
7种形状
1. 钉子型
2. 反Z字型
3. Z字型
4. 反L字型
5. L字型
6. 长条
7. 方块
"""
tetris_shapes = [
    [[1, 1, 1],
     [0, 1, 0]],

    [[0, 2, 2],
     [2, 2, 0]],

    [[3, 3, 0],
     [0, 3, 3]],

    [[4, 0, 0],
     [4, 4, 4]],

    [[0, 0, 5],
     [5, 5, 5]],

    [[6, 6, 6, 6]],

    [[7, 7],
     [7, 7]]
]


def create_textures():
    """ Create a list of images for sprites based on the global colors. """
    # 创建纹理。具体来说，相当于对7种颜色的方块提前进行了图像创建
    new_textures = []
    for color in colors:
        # PIL.Image.new(mode, size, color)​ 是用于创建一个 ​新的空白图像
        image = PIL.Image.new('RGBA', (WIDTH, HEIGHT), color)
        new_textures.append(arcade.Texture(image))
    return new_textures

# 初始化纹理列表，size=len(colors)=8
texture_list = create_textures()


def rotate_counterclockwise(shape):
    """ Rotates a matrix clockwise """
    # 逆时针旋转矩阵形状。
    """
    实际旋转一个矩阵，还需要考虑是左转右转。
    先解析代码含义：
    以z字为例。初始是：
    3   3   0
    0   3   3
    旋转后，为：（y:0->1,x:2->0）
    0   3
    3   3
    3   0
    看出来是一次逆时针旋转。
    如果要顺时针，则遍历顺序改为：（y:1->0,x:0->2）

    此算法可以作为一个比较成熟的旋转算法。
    """
    return [[shape[y][x] for y in range(len(shape))]
            for x in range(len(shape[0]) - 1, -1, -1)]


def check_collision(board, shape, offset):
    """
    See if the matrix stored in the shape will intersect anything
    on the board based on the offset. Offset is an (x, y) coordinate.
    """
    # 检查碰撞
    # board是面板上已经存在的砖块
    # shape是形状。注：形状本身并不保存形状所在位置的信息，而是通过offset拼接计算
    off_x, off_y = offset
    for cy, row in enumerate(shape):
        for cx, cell in enumerate(row):
            # 如果cell>0 且 面板上形状相对位置+偏移量位置有块，则返回True
            if cell and board[cy + off_y][cx + off_x]:
                return True
    return False


def remove_row(board, row):
    """ Remove a row from the board, add a blank row on top. """
    # 删除一行，并在顶部添加一个空白行
    # 这里的del算法需要注意是将列表的某个元素删除，并将剩下的部分拼接，符合自然数学的思路，但在计算机语言中还是比较少见这么方便的方法。
    del board[row]
    return [[0 for _ in range(COLUMN_COUNT)]] + board


def join_matrixes(matrix_1, matrix_2, matrix_2_offset):
    """ Copy matrix 2 onto matrix 1 based on the passed in x, y offset coordinate """
    # 拷贝矩阵2，将之拼接到矩阵1上
    # 通常来说，矩阵1就是board，矩阵2就是下落块
    # 因此矩阵2仅记录了自己的内部信息，未记录下落块在面板中的位置
    # matrix_2_offset就是下落块的坐标
    offset_x, offset_y = matrix_2_offset
    for cy, row in enumerate(matrix_2):
        for cx, val in enumerate(row):
            # 此处相当于面板记录拼接后的信息
            # cy+offset_y-1这里有-1，是碰撞后需要退一格（可以理解为已经发生了碰撞，且是底部碰撞，则向上偏移一格）
            matrix_1[cy + offset_y - 1][cx + offset_x] += val
    return matrix_1


def new_board():
    """ Create a grid of 0's. Add 1's to the bottom for easier collision detection. """
    # Create the main board of 0's
    # 初始化面板，均填0
    board = [[0 for _x in range(COLUMN_COUNT)] for _y in range(ROW_COUNT)]
    # Add a bottom border of 1's
    # 在底部添加一个方块墙（推测是方便计算碰撞）
    board += [[1 for _x in range(COLUMN_COUNT)]]
    return board


class GameView(arcade.View):
    """ Main application class. """

    def __init__(self):
        # 初始化：父级+背景颜色
        super().__init__()
        self.window.background_color = arcade.color.WHITE

        # 声明：面板，开始帧，游戏结束标记，暂停标记，面板精灵列表
        self.board = None
        self.start_frame = 0
        self.game_over = False
        self.paused = False
        self.board_sprite_list = None

        # 声明：下落块和下落块坐标
        self.stone = None
        self.stone_x = 0
        self.stone_y = 0

        # 声明：下落块石头候选列表
        self.stones = []

    def new_stone(self):
        """
        Randomly grab a new stone from the bag of stones,
        if the bag is empty refill the bag and shuffle it.
        Then set the stone's location to the top.
        If we immediately collide, then game-over.
        随机从下落块候选包中获取一个新的下落块。
        如果候选包是空的，则重新填充候选包并打乱顺序。
        将下落块的位置都设置在顶部。
        如果开始时马上检测到要碰撞，则游戏结束。
        """
        # 如果候选包为空，则从外部候选包中拷贝一份下落块列表
        if not self.stones:
            self.stones = tetris_shapes[:]

        # 随机打散下落块列表
        random.shuffle(self.stones)
        # 从下落块列表尾部弹出一个元素
        self.stone = self.stones.pop()
        # 下落块栅格x坐标是中间位置（或者尽量中间的位置）
        self.stone_x = int(COLUMN_COUNT / 2 - len(self.stone[0]) / 2)
        # y坐标初始为0
        self.stone_y = 0

        # 检测碰撞（如果开始时就碰撞，则视为游戏结束）
        if check_collision(self.board, self.stone, (self.stone_x, self.stone_y)):
            self.game_over = True

    def setup(self):
        # 变量初始化
        # 面板初始化
        self.board = new_board()
        # 开始帧初始化
        self.start_frame = GLOBAL_CLOCK.ticks

        # 面板精灵列表初始化（此处遍历所有方块，而不是仅底部方块，可能是为以后的随机初始阵型扩展做准备）
        self.board_sprite_list = arcade.SpriteList()
        for row in range(len(self.board)):
            for column in range(len(self.board[0])):
                # 精灵默认使用第一种纹理
                sprite = arcade.Sprite(texture_list[0])
                # 这里提前将纹理列表赋值到精灵的textures，方便切换纹理
                sprite.textures = texture_list
                # 精灵x坐标
                sprite.center_x = (MARGIN + WIDTH) * column + MARGIN + WIDTH // 2
                # TODO 精灵y坐标。这里的像素y坐标计算，似乎比实际的高
                sprite.center_y = WINDOW_HEIGHT - (MARGIN + HEIGHT) * row + MARGIN + HEIGHT // 2

                # 添加精灵（所有方块都添加了精灵，包括空的方块内容）
                self.board_sprite_list.append(sprite)

        # 初始化下落块
        self.new_stone()
        # 更新面板，方法在下部
        self.update_board()

    def drop(self):
        """
        Drop the stone down one place.
        Check for collision.
        If collided, then
          join matrixes
          Check for rows we can remove
          Update sprite list with stones
          Create a new stone
        将下降块自然下降的一个位置
        检查碰撞
        如果碰撞，则：
            拼接矩阵
            检查可删除行
            更新精灵
            创建新的下落块
        """
        # 仅当游戏未结束，且未暂停的情况触发
        if not self.game_over and not self.paused:
            # 下落块y坐标+1
            self.stone_y += 1
            # 检测碰撞（如果碰撞则……）
            if check_collision(self.board, self.stone, (self.stone_x, self.stone_y)):
                # 将下落块和面板进行拼接
                self.board = join_matrixes(self.board, self.stone, (self.stone_x, self.stone_y))
                # 检测有无待移除行（不包含最后一行，因为是底部）
                # 注：此处使用了while True...break...来实现循环，是因为remove_rows方法中对数组进行了改变，因而不能直接遍历
                while True:
                    for i, row in enumerate(self.board[:-1]):
                        # 通过0 not in判断都是块
                        if 0 not in row:
                            # 移除
                            self.board = remove_row(self.board, i)
                            break
                    else:
                        break
                # 更新面板信息
                self.update_board()

                # 创建新的下落块
                self.new_stone()

    def rotate_stone(self):
        """ Rotate the stone, check collision. """
        # 如果游戏未结束，且未暂停
        if not self.game_over and not self.paused:
            # 石头进行旋转
            new_stone = rotate_counterclockwise(self.stone)
            # 如果旋转后的x坐标+x偏移，超出了总列数：则依据位置向左横移x坐标（这也是碰撞的一种）
            if self.stone_x + len(new_stone[0]) >= COLUMN_COUNT:
                self.stone_x = COLUMN_COUNT - len(new_stone[0])
            # 如果为旋转后的石块和面板，没有发生碰撞，则更新下落块内容；否则不发生变化
            if not check_collision(self.board, new_stone, (self.stone_x, self.stone_y)):
                self.stone = new_stone

    def on_update(self, delta_time):
        """ Update, drop stone if warranted """
        # 数据更新，每过10帧，则触发下降方法
        if GLOBAL_CLOCK.ticks_since(self.start_frame) % 20 == 0:
            self.drop()

    def move(self, delta_x):
        """ Move the stone back and forth based on delta x. """
        # 移动：将下落块向左或向右移动
        # 仅当游戏未结束且未暂停时触发，delta_x就是横向坐标偏移量，只能取1或-1
        if not self.game_over and not self.paused:
            # 横向位置移动
            new_x = self.stone_x + delta_x
            # 如果到左侧边界，则维持在最左侧
            if new_x < 0:
                new_x = 0
            # 如果到右侧边界，则维持在最右侧
            if new_x > COLUMN_COUNT - len(self.stone[0]):
                new_x = COLUMN_COUNT - len(self.stone[0])
            # 如果发生没有碰撞，则刷新x坐标
            if not check_collision(self.board, self.stone, (new_x, self.stone_y)):
                self.stone_x = new_x

    def on_key_press(self, key, modifiers):
        """
        Handle user key presses
        User goes left, move -1
        User goes right, move 1
        Rotate stone,
        or drop down
        处理用户按键
        按左键，触发向左移动
        按右键，触发向右移动
        按上键，触发旋转（逆时针）
        按下件，触发下降
        """
        if key == arcade.key.LEFT:
            self.move(-1)
        elif key == arcade.key.RIGHT:
            self.move(1)
        elif key == arcade.key.UP:
            self.rotate_stone()
        elif key == arcade.key.DOWN:
            self.drop()

    def draw_grid(self, grid, offset_x, offset_y):
        """
        Draw the grid. Used to draw the falling stones. The board is drawn
        by the sprite list.
        绘画网格。仅用来绘画下落块。（变化频繁）
        面板通过精灵列表绘画。（变化不频繁）
        """
        # Draw the grid
        for row in range(len(grid)):
            for column in range(len(grid[0])):
                # 遍历行列属性
                # Figure out what color to draw the box
                # 如果面板内值不为0
                if grid[row][column]:
                    # 取颜色
                    color = colors[grid[row][column]]
                    # Do the math to figure out where the box is
                    # 根据像素的x和y坐标
                    x = (
                        (MARGIN + WIDTH) * (column + offset_x)
                        + MARGIN + WIDTH // 2
                    )
                    # TODO 此处计算的y坐标，似乎要比真实高度要高一层级，待分析
                    y = (
                        WINDOW_HEIGHT - (MARGIN + HEIGHT) * (row + offset_y)
                        + MARGIN + HEIGHT // 2
                    )

                    # Draw the box
                    # 画矩形
                    arcade.draw_rect_filled(arcade.rect.XYWH(x, y, WIDTH, HEIGHT), color)

    def update_board(self):
        """
        Update the sprite list to reflect the contents of the 2d grid
        """
        for row in range(len(self.board)):
            for column in range(len(self.board[0])):
                # 遍历面板属性，取值，赋值纹理。根据坐标赋值纹理
                v = self.board[row][column]
                i = row * COLUMN_COUNT + column
                self.board_sprite_list[i].set_texture(v)

    def on_draw(self):
        """ Render the screen. """

        # This command has to happen before we start drawing
        self.clear()
        # 画面板精灵列表
        self.board_sprite_list.draw()
        # 画矩阵（即下落块）
        self.draw_grid(self.stone, self.stone_x, self.stone_y)


def main():
    """ Create the game window, setup, run """
    # 固定方法调用：window->view->setup_view->show_view->run
    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
    game = GameView()
    game.setup()

    window.show_view(game)
    arcade.run()


if __name__ == "__main__":
    main()